/* 
 * Copyright 2009 Shared Object (www.sharedobject.ch)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package edos.prm.artifact;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;

import edos.prm.activity.Activity;
import edos.prm.activity.ActivityManager.PrmActivityManager;
import edos.prm.engine.PRM;
import edos.prm.engine.PrmSecurityManager;
import edos.prm.role.RoleException;
import edos.prm.task.Task.PrmTask;
import edos.prm.util.Time;
import edos.prm.util.VersionNumber;

/**
 * The {@code ArtifactManager} is responsible for artifacts in the PRM.
 * <p>
 * The {@code ArtifactManager} permits all types of artifacts to be manipulated
 * within the confines of the PRM engine. Notably,
 * <ul>
 * <li>Any type of artifact may be manipulated via this activity.</li>
 * <li>The activity ensures that any registered artifact is accessible to
 * the information system via {@code lookup} and {@code update}.</li>
 * </ul>
 * <p>
 * Other activities that provide operations to create artifacts are in fact
 * clients of {@code ArtifactManager}, as any artifact created by application
 * logic must be registered with {@code ArtifactManager} to be usable by
 * the PRM.
 *
 * @version 1.0
 */
public interface ArtifactManager extends Activity {

	/**
	 * Signals a new artifact type to the PRM engine system.
	 * 
	 * @param type a class that extends {@code Artifact}
	 * @throws RoleException if the calling actor does not have the required
	 * role to register this type.
	 * @throws IllegalArgumentExceptioni if the type does not
	 * implement {@code Artifact}.
	 */
	public void registerArtifactType(Class<? extends Artifact> type)
	throws RoleException, IllegalArgumentException;

	/**
	 * Creates a new artifact of the specified type with the given arguments
	 * 
	 * @param artifactType the artifact type to create
	 * @param args the arguments to pass to the constructor
	 * @return a new artifact of the given type
	 * @throws UnregisteredArtifact if the type has not been registered.
	 * @throws RoleException if the caller does not have permission to execute
	 * this operation.
	 */
	public <E extends Artifact> E newArtifact(Class<E> artifactType,	Object...args)
	throws UnregisteredArtifactType, RoleException;

	/**
	 * Registers an artifact that was created outside of the PRM engine.
	 * 
	 * @param artifact an artifact to be registered with the PRM
	 * @throws UnregisteredArtifact if the type has not been registered.
	 * @throws RoleException if the caller does not have permission to execute
	 * this operation.
	 */
	public <E extends Artifact> void registerArtifact(E artifact)
	throws UnregisteredArtifactType, RoleException;

	/**
	 * The PRM's core lookup operation.
	 * <p>
	 * The operation takes a set of pairs of the form (property --> value),
	 * where value can be a specific object value or a {@code dont-care} value
	 * that matches with any value.
	 * <p>
	 * The operation returns a directory of artifacts, where there is an entry
	 * for each property specified in the arguments to the method. In the
	 * directory, the artifact set that maps to a property contains all of the
	 * artifacts that match the value specified for that property.
	 * <p>
	 * The {@code lookup} operation is non-blocking -- if there are no matching
	 * artifacts then an empty set is returned. Further, it does not remove
	 * any of the artifacts listed in the returned directory from the PRM
	 * engine.
	 * 
	 * @param attributes a series of attribute pairs
	 * @return a directory of artifacts matching the attribute pairs
	 * @throws RoleException if the caller does not have permission to execute
	 * this operation.
	 */
	public Directory lookup(AttributePair<?>...attributes) throws RoleException;

	/**
	 * Verifies if the specified artifact has attributes that match the given
	 * attribute values.
	 * 
	 * @param artifact an artifact object
	 * @param attributes a series of attribute pairs
	 * @return true if the artifact's attributes match with the argument
	 * attributes
	 * @throws RoleException if the caller does not have permission to execute
	 * this operation.
	 */
	public <E extends Artifact> boolean match(E artifact,
			AttributePair<?>...attributes) throws RoleException;

	/**
	 * Tests to see if the specified artifact has been registered in the PRM's
	 * engine.
	 * 
	 * @param artifact an artifact
	 * @return true if this artifact has been registered
	 * @throws RoleException if the caller does not have permission to execute
	 * this operation.
	 */
	public <E extends Artifact> boolean exists(E artifact) throws RoleException;

	/**
	 * Updates the attributes of an artifact
	 * 
	 * @param artifact the artifact object to update
	 * @param attributes the new attributes for the artifact
	 * @throws RoleException if the caller does not have permission to execute
	 * this operation.
	 * @throws IllegalArgumentException if the attribute pair properties are
	 * not present in the artifact.
	 */
	public void update(Artifact artifact, AttributePair<?>...attributes)
	throws RoleException, IllegalArgumentException;

	/**
	 * Removes an artifact from the PRM engine's environment.
	 * 
	 * @param artifact the artifact to remove from the world
	 * @throws RoleException if the caller does not have permission to execute
	 * this operation.
	 */
	public void removeArtifact(Artifact artifact) throws RoleException;

	/**
	 * Apply an SQL or JPQL query to the set of artifacts in the PRM's
	 * information base.
	 * 
	 * @param query the SQL or JPQL query request that is applied to the PRM
	 * @return a set of artifacts resulting from the query.
	 * @throws RoleException if the caller does not have permission to execute
	 * this operation.
	 */
	public ArtifactSet<?> executeQuery(Query query) throws RoleException;
	
	/**
	 * Default Java implementation of the {@code ArtifactManager}.
	 * <p>
	 * The attributes of the implementation are represented as getter method
	 * names.
	 * 
	 * @see Attribute
	 */
	public static class PrmArtifactManager implements ArtifactManager {

		private static PrmArtifactManager instance;

		private Time date;
		private VersionNumber version;
		private String description;

		// The Data structures: The attrsToArtClasses structure maps names of
		// attributes to classes whose instances possess that attribute. The
		// second structure maps classes to instances. This arrangement is
		// needed to help implement the lookup operation.
		private HashMap<String, HashSet<Class<? extends Artifact>>>
		attrsToArtClasses;
		private HashMap<Class<? extends Artifact>, HashSet<? extends Artifact>>
		classesToArtifacts;

		static {
			instance = new PrmArtifactManager();
			instance.registerArtifactType(PrmTask.class);
		}

		private PrmArtifactManager() {
			this.classesToArtifacts =
				new HashMap<Class<? extends Artifact>,
				HashSet<? extends Artifact>>();
			this.attrsToArtClasses =
				new HashMap<String, HashSet<Class<? extends Artifact>>>();
			this.date = new Time();
			this.version = PRM.getPRMVersion();
			this.description = 
				"Default PRM Implementation of Artifact Manager";
			// Must register all activities
			PrmActivityManager.getInstance().register(this);
		}

		public static PrmArtifactManager getInstance() {
			return instance;
		}

		public <E extends Artifact> boolean exists(E art) {
			PrmSecurityManager.roleCheck("exists", getClass(), Artifact.class);
			Class<? extends Artifact> clazz = art.getClass();
			HashSet<? extends Artifact> set = this.classesToArtifacts.get(clazz);
			if ( set == null )
				return false;
			return set.contains(art);
		}

		public synchronized Directory lookup(AttributePair<?>...attributes)
		throws RoleException {
			PrmSecurityManager.
			roleCheck("lookup", getClass(), AttributePair[].class);
			// Create the directory - an entry for each context in attr. pair
			Directory result = new Directory();
			for ( AttributePair<?> pair : attributes ) {
				String property = pair.getProperty();
				ArtifactSet<Artifact> artSet = new ArtifactSet<Artifact>();
				HashSet<Class<? extends Artifact>> classesWithAttribute =
					this.attrsToArtClasses.get(property);
				if ( classesWithAttribute == null )
					break;
				for ( Class<? extends Artifact> clazz : classesWithAttribute ) {
					HashSet<? extends Artifact> objectsWithAttribute =
						this.classesToArtifacts.get(clazz);
					for ( Artifact art : objectsWithAttribute ) {
						if ( !pair.hasDontCareValue()) {
							try {
								Method m = 
									clazz.getMethod(property, new Class[]{});
								Object reply = m.invoke(art, new Object[] {});
								if ( reply.equals(pair.getValue() ) ) 
									artSet.add((Artifact)art);
							} catch (Exception e) {
								e.printStackTrace();
								throw new 
								IllegalArgumentException("Lookup failed: " +
										"for attribute " + property);
							}		
						} else
							artSet.add(art);
					}
					result.add(property, artSet);
				}
			}
			return result;
		}
		
		public <E extends Artifact> boolean match(E artifact,
				AttributePair<?>...attributes) throws RoleException {
			// Go through the list of pairs
			Class<? extends Artifact> clazz = artifact.getClass();
			for ( AttributePair<?> pair : attributes ) {
				// does artifact possess that attribute
				if ( !this.attrsToArtClasses.
						get(pair.getProperty()).contains(clazz) )
					return false;
				// only compare if not a don't care value
				if ( !pair.hasDontCareValue() ) {
					Method m;
					try {
						m = clazz.getMethod(pair.getProperty(), new Class[]{});
						Object reply = m.invoke(artifact, new Object[] {});
						if ( !reply.equals(pair.getValue() ) ) 
							return false;
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
					
			}
			return true;
		}
		
		public synchronized <E extends Artifact>
		E newArtifact(Class<E> artifactType, Object...args)
		throws UnregisteredArtifactType, RoleException {
			PrmSecurityManager.roleCheck("newArtifact", getClass(),
					new Class[]{Class.class, Object[].class});
			try {
				if ( this.classesToArtifacts.keySet().contains(artifactType) ) {
					Class<?>[] parameterTypes = new Class[args.length];
					for ( int i = 0; i < args.length; i++ )
						parameterTypes[i] = args[i].getClass();
					// Note - must ensure only a single constructor for types
					@SuppressWarnings("unchecked")
					Constructor<E> m = artifactType.getConstructors()[0];
					E artifact = m.newInstance(args);
					@SuppressWarnings("unchecked")
					HashSet<E> set =
						(HashSet<E>) this.classesToArtifacts.get(artifactType);
					set.add(artifact);
					notifyAll();
					return artifact;
				} else
					throw new UnregisteredArtifactType(artifactType.getName());
			} catch (Exception e) {
				e.printStackTrace();
				throw new PrmRuntimeException("Cannot instantiate " 
						+ artifactType.getName());
			}
		}

		public synchronized <E extends Artifact> void registerArtifact(E artifact)
		throws UnregisteredArtifactType, RoleException {
			PrmSecurityManager.roleCheck("registerArtifact", getClass(),
					Artifact.class);

			try {
				Class<? extends Artifact> clazz = artifact.getClass();
				if ( this.classesToArtifacts.keySet().contains(clazz) ) {
					@SuppressWarnings("unchecked")
					HashSet<E> set = (HashSet<E>) this.classesToArtifacts.get(clazz);
					set.add(artifact);
					notifyAll();
				} else
					throw new
					UnregisteredArtifactType(artifact.getClass().getName());
			} catch (Exception e) {
				e.printStackTrace();
				throw new PrmRuntimeException("Cannot instantiate " +
						artifact.getClass().getName());
			}
		}

		public void registerArtifactType(Class<? extends Artifact> type) {
			PrmSecurityManager.roleCheck("registerArtifactType", getClass(),
					Class.class);
			this.classesToArtifacts.put(type, new HashSet<Artifact>());
			// Get the properties
			Method[] methods = type.getDeclaredMethods();
			for ( Method m : methods ) {
				if ( m.isAnnotationPresent( Attribute.class ) ) {
					String property = m.getName();
					HashSet<Class<? extends Artifact>> set =
						this.attrsToArtClasses.get(property);
					if ( set == null )
						set = new HashSet<Class<? extends Artifact>>();
					set.add(type);
					this.attrsToArtClasses.put(property, set);
				}
			}
			HashSet<Class<? extends Artifact>> set =
				this.attrsToArtClasses.get("getClass");
			if ( set == null )
				set = new HashSet<Class<? extends Artifact>>();
			set.add(type);
			this.attrsToArtClasses.put("getClass", set);
		}

		public void removeArtifact(Artifact artifact) {
			PrmSecurityManager.roleCheck("removeArtifact", getClass(),
					Artifact.class);

			Class<?> clazz = artifact.getClass();
			HashSet<? extends Artifact> set = this.classesToArtifacts.get(clazz);
			if ( set == null )
				return;
			set.remove(artifact);
		}

		public void update(Artifact artifact, AttributePair<?>... attributes) {
			PrmSecurityManager.roleCheck("update", getClass(),
					new Class[] {Artifact.class, AttributePair[].class});
			for ( AttributePair<?> pair : attributes ) {
				// get attribute
				String methodName = pair.getProperty();
				methodName.replaceFirst("get", "set");
				Object arguments = pair.getValue();
				try {
					Method method = 
						artifact.getClass().getDeclaredMethod(methodName,
								arguments.getClass());
					method.invoke(artifact, arguments);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		
		public Time getCreationDate() {
			return date;
		}

		public String getDescription() {
			return description;
		}

		public VersionNumber getVersion() {
			return version;
		}

		public ArtifactSet<?> executeQuery(Query query) throws RoleException {
			throw new 
			UnsupportedOperationException("Not implemented in Java SE version");
		}
	}
}
